package jmine.tec.sync;

import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import javassist.util.proxy.ProxyObject;
import jmine.tec.rpc.common.entity.EntityRepresentation;
import jmine.tec.rpc.common.serialization.Serializer;
import jmine.tec.sync.dao.SynchronizationErrorEntityDAO;
import jmine.tec.sync.domain.SynchronizationErrorEntity;
import jmine.tec.sync.domain.enumx.SynchronizationOperationType;
import jmine.tec.utils.collection.impl.SmallSet;
import jmine.tec.utils.register.Receiver;
import net.sf.cglib.proxy.Enhancer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Hibernate;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.authorization.AuthorizationManager;
import bancosys.tec.persist.authorization.AuthorizationStatus;
import bancosys.tec.rtm.impl.RtmController;

/**
 * Classe responsavel por ouvir as mudancas vindas do sistema remoto. O {@link jmine.tec.rpc.common.provider.RestServiceProvider}
 * configurado deve ter acesso ao sistema remoto indicado pelo valor {@link #source}
 * 
 * @author takeshi
 */
public class DatabaseSynchronizationListener implements Receiver<EntityMaterializer<?>> {

    private static final Log LOGGER = LogFactory.getLog(DatabaseSynchronizationListener.class);

    private final Map<String, EntityMaterializer<?>> materializerRegistry = new HashMap<String, EntityMaterializer<?>>();

    private String source;

    private SynchronizationErrorEntityDAO synchronizationErrorEntityDAO;

    private Serializer serializer;

    private RtmController rtmController;

    private final ThreadLocal<Map<Object, Object>> representationCache = new ThreadLocal<Map<Object, Object>>();

    private Set<String> ignoredClasses = new SmallSet<String>();

    /**
     * Devolve o cache associado a thread atual
     * 
     * @return {@link Map}
     */
    public Map<Object, Object> getRepresentationCache() {
        return this.representationCache.get();
    }

    /**
     * Inicializa o cache
     */
    private void initRepresentationCache() {
        this.representationCache.set(new HashMap<Object, Object>());
    }

    /**
     * Remove o cache
     */
    private void clearRepresentationCache() {
        this.representationCache.remove();
    }

    /**
     * {@inheritDoc}
     */
    public void onModification(String[] classHierarchy, Serializable remoteId, Serializable naturalKey) {
        if (this.isIgnored(classHierarchy)) {
            LOGGER.info("Ignoring class: " + classHierarchy[0] + " with id: " + remoteId + " and naturalKey: " + naturalKey);
            return;
        }
        this.initRepresentationCache();
        AuthorizationManager.setCurrentThreadAuthorizationStatus(AuthorizationStatus.DISABLED);
        try {
            EntityMaterializer<?> materializer = this.getEntityMaterializer(classHierarchy);
            if (materializer != null) {
                materializer.updateEntity(remoteId, naturalKey);
            } else {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Materializer not registered for " + classHierarchy[0]);
                }
            }
        } finally {
            this.clearRepresentationCache();
            AuthorizationManager.setCurrentThreadAuthorizationStatus(AuthorizationStatus.ENABLED);
        }
    }

    /**
     * Verifica se o objeto deve ser ignorado
     * 
     * @param classHierarchy array de {@link String}
     * @return boolean
     */
    protected boolean isIgnored(String[] classHierarchy) {
        for (String string : classHierarchy) {
            if (this.ignoredClasses.contains(string)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Devolve o {@link EntityMaterializer} associado as classes passadas
     * 
     * @param classHierarchy array de {@link String}
     * @return {@link EntityMaterializer}
     */
    private EntityMaterializer<?> getEntityMaterializer(String[] classHierarchy) {
        for (String string : classHierarchy) {
            EntityMaterializer materializer = this.getEntityMaterializer(string);
            if (materializer != null) {
                return materializer;
            }
        }
        return null;
    }

    /**
     * Devolve o {@link EntityMaterializer} associado a classe passada
     * 
     * @param remoteClass String
     * @return {@link EntityMaterializer}
     */
    private EntityMaterializer getEntityMaterializer(String remoteClass) {
        return this.materializerRegistry.get(remoteClass);
    }

    /**
     * Sinaliza que uma entidade foi apagada
     * 
     * @param classHierarchy as classes
     * @param remoteId o id no sistema remoto
     * @param naturalKey {@link Serializable}
     */
    public void entityDeleted(String[] classHierarchy, Serializable remoteId, Serializable naturalKey) {
        if (this.isIgnored(classHierarchy)) {
            return;
        }
        EntityMaterializer materializer = this.getEntityMaterializer(classHierarchy);
        if (materializer != null) {
            if (naturalKey != null) {
                materializer.entityDeleted(remoteId, naturalKey);
            } else {
                materializer.entityDeleted(remoteId);
            }
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Materializer not registered for " + classHierarchy[0]);
            }
        }
    }

    /**
     * @return {@link String}
     */
    public String getSource() {
        return this.source;
    }

    /**
     * @param source {@link String}
     */
    public void setSource(String source) {
        this.source = source;
    }

    /**
     * {@inheritDoc}
     */
    public void register(Collection<? extends EntityMaterializer<?>> registers) {
        for (EntityMaterializer<?> entityMaterializer : registers) {
            this.materializerRegistry.put(entityMaterializer.getRemoteEntityName(), entityMaterializer);
            entityMaterializer.setDatabaseSynchronizationListener(this);
        }
    }

    /**
     * Devolve um materializer para a classe remota passada
     * 
     * @param remoteClass String
     * @return {@link EntityMaterializer}
     */
    public EntityMaterializer<?> getMaterializerFor(String remoteClass) {
        return this.materializerRegistry.get(remoteClass);
    }

    /**
     * Devolve um {@link EntityMaterializer} responsavel por materializer classes do tipo passado - ou null se nenhum for encontrado
     * 
     * @param <A> o tipo
     * @param type Class
     * @return {@link EntityMaterializer} ou <code>null</code>
     */
    @SuppressWarnings("unchecked")
    public <A extends Persistable> EntityMaterializer<A> getMaterializerForLocalClass(Class<A> type) {
        String localEntityName = type.getName();
        for (EntityMaterializer<?> e : this.materializerRegistry.values()) {
            if (e.getLocalEntityName().equals(localEntityName)) {
                return (EntityMaterializer<A>) e;
            }
        }
        throw new RuntimeException("Materializer not found for local entity: " + type);
    }

    /**
     * @return the materializerRegistry
     */
    public Map<String, EntityMaterializer<?>> getMaterializerRegistry() {
        return this.materializerRegistry;
    }

    /**
     * @return the ignoredClasses
     */
    public Set<String> getIgnoredClasses() {
        return this.ignoredClasses;
    }

    /**
     * @param rtmController the rtmController to set
     */
    public void setRtmController(RtmController rtmController) {
        this.rtmController = rtmController;
    }

    /**
     * @param ignoredClasses the ignoredClasses to set
     */
    public void setIgnoredClasses(Set<String> ignoredClasses) {
        this.ignoredClasses = ignoredClasses;
    }

    /**
     * Devolve um materializer associado a uma classe
     * 
     * @param representationType {@link Class}
     * @return {@link EntityMaterializer}
     */
    public EntityMaterializer<?> getMaterializerForRepresentationType(Class<?> representationType) {
        Class<?> resolved = resolveProxyType(representationType);
        for (EntityMaterializer<?> e : this.materializerRegistry.values()) {
            List<? extends Class<?>> remoteRepresentations = e.remoteRepresentations();
            if (remoteRepresentations.size() == 1 && remoteRepresentations.get(0).equals(resolved)) {
                return e;
            }
        }
        throw new RuntimeException("Materializer not found for remote representation: " + resolved);
    }

    /**
     * Resolve a classe concreta de um proxy criado via CGLIB
     * 
     * @param representationType Class
     * @return Class
     */
    private static Class<?> resolveProxyType(Class<?> representationType) {
        if (Enhancer.isEnhanced(representationType) || ProxyObject.class.isAssignableFrom(representationType)) {
            return resolveProxyType(representationType.getSuperclass());
        }
        return representationType;
    }

    /**
     * @param serializer the jsonSerializer to set
     */
    public void setSerializer(Serializer serializer) {
        this.serializer = serializer;
    }

    /**
     * Metodo responsável por criar a entidade que guada as informações da sincronização, quando essa não é bem sucedida
     * 
     * @param remoteEntityId id da entidade na origem
     * @param entityClass classe da entidade na origem
     * @param message messagem de erro
     * @param entitySource origem
     * @param entityNaturalKey natural key da entidade
     * @param typeOperation tipo da operação que estava sendo realizada
     * @param entity representação da entidade.
     */
    protected void createSynchronizationErrorEntity(final Serializable remoteEntityId, final String entityClass, final String message,
            final String entitySource, final Serializable entityNaturalKey, final SynchronizationOperationType typeOperation,
            final EntityRepresentation entity) {
        try {
            this.rtmController.executeTransacted(new Callable<Void>() {
                public Void call() throws Exception {
                    SynchronizationErrorEntity sync = DatabaseSynchronizationListener.this.synchronizationErrorEntityDAO.createBean();
                    try {
                        sync.setEntity(Hibernate.createBlob(DatabaseSynchronizationListener.this.serializer.serialize(entity)));
                    } catch (IOException e) {
                        DatabaseSynchronizationListener.this.rtmController.monitor(e);
                    }
                    sync.setEntityClass(entityClass);
                    sync.setMessage(message);
                    sync.setEntityName((String) entityNaturalKey);
                    sync.setRemoteEntityId((Long) remoteEntityId);
                    sync.setSource(entitySource);
                    sync.setOperationType(typeOperation);
                    sync.getPersister().save();
                    DatabaseSynchronizationListener.this.rtmController.getSessionFactory().getCurrentSession().flush();
                    return null;
                }
            });
        } catch (Exception e1) {
            this.rtmController.monitor(e1);
        }
    }

    /**
     * @param requestedId a requestedId
     * @param representation a representacao
     * @param naturalKey natural key da entidade
     * @param message a message
     * @param classe a classe
     * @param operation tipo da operação
     * @param e exception que ocoreu no materializar
     */
    public void trackingError(EntityRepresentation representation, Serializable requestedId, Serializable naturalKey, String message,
            String classe, SynchronizationOperationType operation, Exception e) {
        this.rtmController.monitor(e);
        this.createSynchronizationErrorEntity(requestedId, classe, message, this.getSource(), naturalKey, operation, representation);
    }

    /**
     * @return the synchronizationErrorEntityDAO
     */
    public SynchronizationErrorEntityDAO getSynchronizationErrorEntityDAO() {
        return this.synchronizationErrorEntityDAO;
    }

    /**
     * @param synchronizationErrorEntityDAO the synchronizationErrorEntityDAO to set
     */
    public void setSynchronizationErrorEntityDAO(SynchronizationErrorEntityDAO synchronizationErrorEntityDAO) {
        this.synchronizationErrorEntityDAO = synchronizationErrorEntityDAO;
    }

}
